/**
 * 
 */
package pacMan;


import javax.sound.sampled.Clip;
import javax.sound.sampled.LineEvent;
import javax.sound.sampled.LineListener;
import javax.swing.Timer;


/**
 * Class: GameStateManager
 * 
 * Partner class to GameBoard, this encapsulates the various
 * game states and manages the timing logic for managing the
 * game board in response to player input and Ghost movement
 * logic.
 * 
 * @author 		Tomas Rivera
 * @version 	$Id$
 *
 */
public class GameStateManager {
	
	//////////////////////////////////////////////////////////
	// Enumeration definitions
	//////////////////////////////////////////////////////////
	
	public static enum GAME_EVENT {
		NONE,
		ENERGIZER_CONSUMED,
		PELLET_CONSUMED,
		GHOST_CONSUMED,
		GHOST_REACHED_HOME,
		BONUS_ITEM_CONSUMED,
		PLAYER_KILLED,
		PLAYER_DEATH_SEQUENCE_DONE,
		TIMER_INCREMENT,
		GAME_BOARD_INITIALIZED,
		GAME_BOARD_RESET,
		GAME_BOARD_READY,
		NEW_GAME_START,
		DEBUG_ROUND_COMPLETE;
	}
	
	public static enum NAV_MODE {
		WANDER_MAZE,
		PACE_HOME,
		GO_HOME,
		GO_HOME_CENTER,
		ENTER_HOME,
		LEAVE_HOME;
	}
	
	public static enum MOVE_BEHAVIOR {
		SCATTER,
		CHASE;
	}
	
	public static enum GAME_STATE {
		DEMO_SEQUENCE,
		WAIT_STARTING,
		RUNNING,
		PLAYER_DEATH_SEQUENCE,
		INTERMISSION_SEQUENCE,
		WAIT_GAME_OVER,
		WAIT_ROUND_COMPLETED,
		WAIT_FOR_GAME_CONTINUE;
	}
	
	public static enum GAME_BACKGROUND_SOUND {
		NONE,
		SIREN_SLOW,
		SIREN_MEDIUM,
		SIREN_FAST,
		ENERGIZED,
		GHOST_HEADING_HOME;
	}
	
	//////////////////////////////////////////////////////////
	// Constructor methods
	//////////////////////////////////////////////////////////
	
	/**
	 * Creates a new GameStateManager.
	 * 
	 * @param gameBoard
	 */
	public GameStateManager(GameBoard gameBoard) {
		// TODO: this should probably be a singleton...
		
		ghostGlobalState = MOVE_BEHAVIOR.SCATTER;
		currentGameState = GAME_STATE.DEMO_SEQUENCE;
		currentGameRound = 1;
		this.gameBoard = gameBoard;
		mazeComplete = false;
		ghostsHeadingHome = 0;
		playerKilledThisRound = false;
		
		releaseGhostPelletCounter = 0;
		releaseGhostSecondCounter = 0;
		ghostScaredSecondCounter = 0;
		ghostScatterChaseSecondCounter = 0;
		ghostsReleased = 1;
		releasePinky = 0;
		releaseInky = 0;
		releaseClyde = 0;
		
		initializeSoundEffectEventListeners();
	}
	
	//////////////////////////////////////////////////////////
	// Public / Protected methods
	//////////////////////////////////////////////////////////
	
	/**
	 * Get a count of the Ghosts that are on their way back to the pen.
	 * 
	 * @return
	 */
	public int getGhostHeadingHomeCount() { return ghostsHeadingHome; }
	
	/**
	 * Check if the round completion parameters have been satisfied, i.e.
	 * have all the pellets been consumed.
	 * 
	 * @return
	 */
	public boolean mazeComplete() { return mazeComplete; }
	
	/**
	 * Flag that the round has been completed.
	 * 
	 * @param complete
	 */
	public void setMazeComplete(boolean complete) { mazeComplete = complete; }
	
	/**
	 * Check if the game is currently in the "running" state. This means that all
	 * Actors are free to move around the game board and the player is allowed
	 * to consume objects that it runs across.
	 * 
	 * @return
	 */
	public boolean isGameRunning() { return currentGameState == GAME_STATE.RUNNING; }
	
	/**
	 * Get the current Ghost behavior mode, i.e. CHASE or SCATTER.
	 * 
	 * @return
	 */
	public MOVE_BEHAVIOR getGhostGlobalState() { return ghostGlobalState; }
	
	/**
	 * Change the behavioral state for all of the Ghosts.
	 * 
	 * @param newState
	 */
	public void setGhostGlobalState(MOVE_BEHAVIOR newState) { ghostGlobalState = newState; }
	
	/**
	 * Get the current game state, i.e. RUNNING, GAME_OVER, etc.
	 * 
	 * @return
	 */
	public GAME_STATE getCurrentGameState() { return currentGameState; }
	
	/**
	 * Get the current game round.
	 * 
	 * @return
	 */
	public int getCurrentGameRound() { return currentGameRound; }
	
	/**
	 * Handles updating the GAME_STATE depending on what GAME_EVENT was passed.
	 * 
	 * @param event
	 */
	public void handleEvent(GAME_EVENT event) {
		
		switch(event) {
			default:
				break;
				
			case GAME_BOARD_INITIALIZED:
				updateGameState(GAME_STATE.DEMO_SEQUENCE);
				gameBoard.resetActors();
				break;
				
			case NEW_GAME_START:
				updateGameState(GAME_STATE.WAIT_STARTING);
				break;
			
			case ENERGIZER_CONSUMED:
				
				switch(currentGameRound) {
					case 1:
						ghostScaredSecondCounter = 7;
						break;
						
					case 2:
					case 6:
					case 10:
						ghostScaredSecondCounter = 6;
						break;
						
					case 3:
						ghostScaredSecondCounter = 5;
						break;
						
					case 4:
					case 14:
						ghostScaredSecondCounter = 4;
						break;
					
					case 5:
					case 7:
					case 8:
					case 11:
						ghostScaredSecondCounter = 3;
						break;
						
					case 9:
					case 12:
					case 13:
					case 15:
					case 16:
					case 17:
					case 18:
						ghostScaredSecondCounter = 2;
						break;
						
					default:
						handleEvent(GAME_EVENT.PELLET_CONSUMED);
						break;
				}
				
				// Set all ghosts state to frightened
				setGhostsFrightened(true);
					
				
				// Fall through...
				
			case PELLET_CONSUMED:
				if(gameBoard.getPelletCountRemaining() != 0) {
					this.updateGhostBehaviorCounters(false, true);
					break;
				}
				// Otherwise fall through...
			case DEBUG_ROUND_COMPLETE:
				gameBoard.hideEnemyActors();
				setMazeComplete(true);
				currentGameRound++;
				updateGameState(GAME_STATE.WAIT_ROUND_COMPLETED);
				
				break;
				
			case GHOST_CONSUMED:
				// increment count of ghosts heading home
				ghostsHeadingHome++;
				gameBoard.freezeAllActors(30);
				break;
				
			case GHOST_REACHED_HOME:
				ghostsHeadingHome--;
				
				break;
				
			case PLAYER_KILLED:
				// Change background sound to NONE
				updateGameState(GAME_STATE.PLAYER_DEATH_SEQUENCE);
				playerKilledThisRound = true;
				break;
			
			case PLAYER_DEATH_SEQUENCE_DONE:
				if(gameBoard.getPlayerLivesRemaining() == 0) {
					updateGameState(GAME_STATE.WAIT_GAME_OVER);
				} else {
					updateGameState(GAME_STATE.WAIT_FOR_GAME_CONTINUE);
				}
				break;
				
			case TIMER_INCREMENT:
				this.updateGhostBehaviorCounters(true, false);
				break;
			
			case GAME_BOARD_RESET:
				System.out.printf("Starting round %d...%n", currentGameRound);
				// Reset stationary consumable objects and counters.
				gameBoard.resetBoardForNextRound();
				
				playerKilledThisRound = false;
				
				// Move all actors back to default positions
				gameBoard.resetActors();
				updateGameState(GAME_STATE.WAIT_FOR_GAME_CONTINUE);
				break;
				
			case GAME_BOARD_READY:
				updateGameState(GAME_STATE.RUNNING);
				break;
		}
	}
	
	/**
	 * Changes the background sound effect depending on the number of pellets consumed so far.
	 * 
	 */
	public void startSirenBackgroundEffect() {
		if(gameBoard.getPelletCountRemaining() < (gameBoard.getPelletCountGenerated() / 3)) {
			updateBackgroundSoundState(GAME_BACKGROUND_SOUND.SIREN_FAST);

		} else if (gameBoard.getPelletCountRemaining() < (gameBoard.getPelletCountGenerated() * 2 / 3)) {
			updateBackgroundSoundState(GAME_BACKGROUND_SOUND.SIREN_MEDIUM);
		} else {
			updateBackgroundSoundState(GAME_BACKGROUND_SOUND.SIREN_SLOW);
		}
	}
	
	/**
	 * Called after event timer expires. Will hide all enemy Actors from the game board,
	 * play "pacman_death" sound effect and run a short animation. LineListener that is
	 * registered on the SoundEffect will trigger once the Clip completes, and will issue
	 * GAME_EVENT.PLAYER_DEATH_SEQUENCE_DONE
	 * 
	 */
	public void delayedTask_runPlayerDeathAnimation() {
		
		// Hide all ghosts
		gameBoard.hideEnemyActors();
		
		// Play sync'd killed sound effect and animation
		SoundEffect.EVENT_PACMAN_KILLED.play();
	}
	
	/**
	 * Updates the current background sound effect state, and changes it to another sound effect
	 * if passed a different GAME_BACKGROUND_SOUND
	 * 
	 * @param soundState
	 */
	public synchronized void updateBackgroundSoundState(GAME_BACKGROUND_SOUND soundState) {
		
		if(backgroundSoundState == soundState) {
			return;
		}
		
		System.out.printf("Old background soundState = %s, new background soundState = %s%n", backgroundSoundState, soundState);
		backgroundSoundState = soundState;
		
		switch(soundState) {
			default:
				System.out.println("background effect: none");
				if(activeBackgroundSound != null) {
					activeBackgroundSound.stop();
				}
				break;
			case SIREN_SLOW:
				System.out.println("background effect: siren slow");
				if(activeBackgroundSound != null) {
					activeBackgroundSound.stop();
				}
				
				activeBackgroundSound = SoundEffect.BKGROUND_PACMAN_SIREN_SLOW.startLoop();
				break;
			
			case SIREN_MEDIUM:
				System.out.println("background effect: siren medium");
				if(activeBackgroundSound != null) {
					activeBackgroundSound.stop();
				}
				
				activeBackgroundSound = SoundEffect.BKGROUND_PACMAN_SIREN_MEDIUM.startLoop();
				break;
				
			case SIREN_FAST:
				System.out.println("background effect: siren fast");
				if(activeBackgroundSound != null) {
					activeBackgroundSound.stop();
				}
				
				activeBackgroundSound = SoundEffect.BKGROUND_PACMAN_SIREN_FAST.startLoop();
				break;
				
			case ENERGIZED:
				System.out.println("background effect: energized");
				if(activeBackgroundSound != null) {
					activeBackgroundSound.stop();
				}
				
				activeBackgroundSound = SoundEffect.BKGROUND_PACMAN_ENERGIZED.startLoop();
				break;
			case GHOST_HEADING_HOME:
				System.out.println("background effect: ghost eaten");
				if(activeBackgroundSound != null) {
					activeBackgroundSound.stop();
				}
				
				activeBackgroundSound = SoundEffect.BKGROUND_PACMAN_ENERGIZED_GHOST_EATEN.startLoop();
				break;
		}
	}
	
	//////////////////////////////////////////////////////////
	// Private methods
	//////////////////////////////////////////////////////////
	
	/*
	 * Initializes and registers LineListeners for SoundEffects that
	 * will trigger a new GAME_EVENT once they complete.
	 * 
	 */
	private void initializeSoundEffectEventListeners() {
		
		SoundEffect.GAME_START.addLineListener(new LineListener() {

			@Override
			public void update(LineEvent event) {
				if(event.getType() != LineEvent.Type.STOP) {
					return;
				}
				
				handleEvent(GAME_EVENT.GAME_BOARD_READY);
			}
	    });
		
		SoundEffect.EVENT_PACMAN_KILLED.addLineListener(new LineListener() {

			@Override
			public void update(LineEvent event) {
				
				if(event.getType() == LineEvent.Type.START) {
					// Start synchronized animation
					System.out.println("Start animation...");
				}
				else if(event.getType() == LineEvent.Type.STOP) {
					handleEvent(GAME_EVENT.PLAYER_DEATH_SEQUENCE_DONE);
				}
			}
	    });
		
		SoundEffect.INTERMISSION.addLineListener(new LineListener() {

			@Override
			public void update(LineEvent event) {
				if(event.getType() != LineEvent.Type.STOP) {
					return;
				}
				
				updateGameState(GAME_STATE.WAIT_FOR_GAME_CONTINUE);
			}
	    });
	}
	
	/*
	 * Update the current GAME_STATE, and take action based on that state
	 * transition.
	 * 
	 */
	private void updateGameState(GAME_STATE newState) {
		System.out.printf("Old game state = %s, new game state = %s%n", currentGameState, newState);
		
		switch(newState) {
			case DEMO_SEQUENCE:
				
				// Kill any previous background sound that was active
				if(activeBackgroundSound != null) {
					activeBackgroundSound.stop();
				}
				
				gameBoard.setCenterMessage("Hit any key to start");
				//gameBoard.resetBoardForNextRound();
				// Start DEMO animations
				
				break;
				
			case WAIT_STARTING:
				// Stop DEMO animations
				
				// Update center message
				gameBoard.setCenterMessage("READY!");
				gameBoard.repaint();
				
				// Kill any previous background sound that was active
				if(activeBackgroundSound != null) {
					activeBackgroundSound.stop();
				}
				
				// Start "pacman_beginning" opening music. This will trigger
				// transition to GAME_STATE_RUNNING when the clip completes.
				activeBackgroundSound = SoundEffect.GAME_START.play();
				break;
				
			case RUNNING:
				ghostsReleased = 1;
				gameBoard.setCenterMessage(" ");
				
				// Reset ghost pellet and second counters
				releaseGhostPelletCounter = 0;
				releaseGhostSecondCounter = 0;
				ghostScatterChaseSecondCounter = 0;
				configureScatterChaseLimits();
				configurePelletConsumedLimits();
				this.setGhostGlobalState(MOVE_BEHAVIOR.SCATTER);
				break;
				
			case WAIT_ROUND_COMPLETED:
				gameBoard.setCenterMessage(" ");
				// Kill any previous background sound that was active
				if(activeBackgroundSound != null) {
					activeBackgroundSound.stop();
				}
				
				eventTimer = new Timer(5000, (e) -> handleEvent(GAME_EVENT.GAME_BOARD_RESET));
				eventTimer.setRepeats(false);
				eventTimer.start();
				break;
				
			case WAIT_FOR_GAME_CONTINUE:
				gameBoard.setCenterMessage("READY!");
				
				// Kill any previous background sound that was active
				if(activeBackgroundSound != null) {
					activeBackgroundSound.stop();
				}
				
				gameBoard.resetActors();
				eventTimer = new Timer(2500, (e) -> handleEvent(GAME_EVENT.GAME_BOARD_READY));
				eventTimer.setRepeats(false);
				eventTimer.start();
				break;
				
			case PLAYER_DEATH_SEQUENCE:
				// Stop all sounds
				if(activeBackgroundSound != null) {
					activeBackgroundSound.stop();
				}
				
				SoundEffect.killAllBackgroundSoundEffects();
				
				eventTimer = new Timer(1000, (e) -> delayedTask_runPlayerDeathAnimation());
				
				eventTimer.setRepeats(false);
				eventTimer.start();
				break;
			
			case INTERMISSION_SEQUENCE:
				
				
				break;
				
			case WAIT_GAME_OVER:
				
				// Update center message
				gameBoard.setCenterMessage("GAME OVER");
				
				// Kill any previous background sound that was active
				if(activeBackgroundSound != null) {
					activeBackgroundSound.stop();
				}
				
				eventTimer = new Timer(1000, (e) -> handleEvent(GAME_EVENT.GAME_BOARD_INITIALIZED));
				
				eventTimer.setRepeats(false);
				eventTimer.start();
				
				break;
			default:
				break;
		}
		
		// Update gameState
		currentGameState = newState;
	}
	
	/*
	 * During gameplay, the time that a Ghost is in either CHASE or SCATTER mode is limited
	 * based on the current game round. This function is called on game start and at the
	 * beginning of each successive game round and sets the timing of when we have the
	 * Ghosts transition between those modes.
	 * 
	 */
	private void configureScatterChaseLimits() {
		if(currentGameRound > 4) {
			SCATTER_1 = 5;
			CHASE_1 = SCATTER_1 + 20;
			SCATTER_2 = CHASE_1 + 5;
			CHASE_2 = SCATTER_2 + 20;
			SCATTER_3 = CHASE_2 + 5;
			CHASE_3 = SCATTER_3 + 1037;
			SCATTER_4 = CHASE_3 + 1;
		} else if(currentGameRound < 2) {
			SCATTER_1 = 7;
			CHASE_1 = SCATTER_1 + 20;
			SCATTER_2 = CHASE_1 + 7;
			CHASE_2 = SCATTER_2 + 20;
			SCATTER_3 = CHASE_2 + 5;
			CHASE_3 = SCATTER_3 + 20;
			SCATTER_4 = CHASE_3 + 5;
		} else {
			SCATTER_1 = 7;
			CHASE_1 = SCATTER_1 + 20;
			SCATTER_2 = CHASE_1 + 7;
			CHASE_2 = SCATTER_2 + 20;
			SCATTER_3 = CHASE_2 + 5;
			CHASE_3 = SCATTER_3 + 1033;
			SCATTER_4 = CHASE_3 + 1;
		}
	}
	
	/*
	 * Ghosts are allowed to leave the pen if the game has met one of
	 * two conditions: the player has not consumed a pellet in three
	 * seconds, or the player has consumed a predetermined number of
	 * pellets. This consumption limit is based on whether or not the
	 * player has been killed at least once in the current round, and
	 * the current game round.
	 * 
	 */
	private void configurePelletConsumedLimits() {
		if(this.playerKilledThisRound) {
			releasePinky = 7;
			releaseInky = 17;
			releaseClyde = 32;
		} else {
			if(currentGameRound == 1) {
				releasePinky = 0;
				releaseInky = 30;
				releaseClyde = 60;
				
			} else if (currentGameRound == 2) {
				releasePinky = 0;
				releaseInky = 0;
				releaseClyde = 50;
				
			} else {
				releasePinky = 0;
				releaseInky = 0;
				releaseClyde = 0;
			}
		}
	}
	
	/*
	 * Called one each second, and every time we consume a pellet. This manages transitioning
	 * Ghosts between CHASE and SCATTER modes, and for releasing a Ghost each time we hit
	 * the pellet consumption threshold or the Ghost release countdown.
	 * 
	 */
	private synchronized void updateGhostBehaviorCounters(boolean timeIncrement, boolean pelletIncrement) {
		
		if(this.currentGameState != GAME_STATE.RUNNING) return;
		
		if(timeIncrement) {
			if(backgroundSoundState == GAME_BACKGROUND_SOUND.ENERGIZED ||
				backgroundSoundState == GAME_BACKGROUND_SOUND.GHOST_HEADING_HOME)
			{
				ghostScaredSecondCounter--;
				
				if(ghostScaredSecondCounter == 1) {
					gameBoard.setGhostsFlashing(true);
				} else if(ghostScaredSecondCounter == 0) {
					gameBoard.setGhostsFlashing(false);
					setGhostsFrightened(false);
				}
			} else {
				ghostScaredSecondCounter = 0;
				gameBoard.setGhostsFlashing(false);
				ghostScatterChaseSecondCounter++;
				
				if(ghostScatterChaseSecondCounter > SCATTER_4) {
					setGhostGlobalState(MOVE_BEHAVIOR.CHASE);
				} else if(ghostScatterChaseSecondCounter > CHASE_3) {
					setGhostGlobalState(MOVE_BEHAVIOR.SCATTER);
				} else if(ghostScatterChaseSecondCounter > SCATTER_3) {
					setGhostGlobalState(MOVE_BEHAVIOR.CHASE);
				} else if(ghostScatterChaseSecondCounter > CHASE_2) {
					setGhostGlobalState(MOVE_BEHAVIOR.SCATTER);
				} else if(ghostScatterChaseSecondCounter > SCATTER_2) {
					setGhostGlobalState(MOVE_BEHAVIOR.CHASE);
				} else if(ghostScatterChaseSecondCounter > CHASE_1) {
					setGhostGlobalState(MOVE_BEHAVIOR.SCATTER);
				} else if(ghostScatterChaseSecondCounter > SCATTER_1) {
					setGhostGlobalState(MOVE_BEHAVIOR.CHASE);
				}
			}

			releaseGhostSecondCounter++;
			System.out.printf("Release ghost counter %d%n", releaseGhostSecondCounter);
			
			if(currentGameRound > 4) {
				if(releaseGhostSecondCounter > 3 && ghostsReleased < 4) {
					gameBoard.releaseGhost();
					ghostsReleased++;
					releaseGhostSecondCounter = 0;
				}
			} else if(currentGameRound < 2) {
				if(releaseGhostSecondCounter > 4 && ghostsReleased < 4) {
					gameBoard.releaseGhost();
					ghostsReleased++;
					releaseGhostSecondCounter = 0;
				}
			} else {
				if(releaseGhostSecondCounter > 4 && ghostsReleased < 4) {
					gameBoard.releaseGhost();
					ghostsReleased++;
					releaseGhostSecondCounter = 0;
				}
			}
		} 
		else if (pelletIncrement) 
		{	
			releaseGhostSecondCounter = 0;
			releaseGhostPelletCounter++;
			
			if((ghostsReleased == 1 && releaseGhostPelletCounter > releasePinky) ||
			    (ghostsReleased == 2 && releaseGhostPelletCounter > releaseInky) ||
				(ghostsReleased == 3 && releaseGhostPelletCounter > releaseClyde)) 
			{
				gameBoard.releaseGhost();
				ghostsReleased++;
			}
		} 
		else 
		{
			assert false;
		}
		
	}
	/*
	 * Set or clears flag that determines if Ghost is in 'frightened' mode, and
	 * forces the Ghost to reverse their current direction of travel.
	 */
	private void setGhostsFrightened(boolean scared) {
		for(Actor enemyActor : gameBoard.getEnemyActors()) {
			if(!enemyActor.edible() && !enemyActor.hazardous()) {
				continue;
			}
			
			if(currentGameRound <= 18) {
				// After round 18 the Ghosts never become frightened.
				enemyActor.setEdible(scared);
				enemyActor.setHazardous(!scared);
			}
			
			// We always reverse the direction of travel on this call,
			// regardless of what the current game round is.
			enemyActor.reverseDirection();
		}
	}

	//////////////////////////////////////////////////////////
	// Private member variables
	//////////////////////////////////////////////////////////
	
	private int SCATTER_1;
	private int CHASE_1;
	private int SCATTER_2;
	private int CHASE_2;
	private int SCATTER_3;
	private int CHASE_3;
	private int SCATTER_4;
	
	private int releasePinky;
	private int releaseInky;
	private int releaseClyde;
	
	private MOVE_BEHAVIOR ghostGlobalState;
	private GAME_STATE currentGameState;
	private int currentGameRound;
	private GameBoard gameBoard;
	private boolean mazeComplete;
	private boolean playerKilledThisRound;
	private int ghostsHeadingHome;
	private Clip activeBackgroundSound;
	private GAME_BACKGROUND_SOUND backgroundSoundState;
	private Timer eventTimer;
	
	private int releaseGhostPelletCounter;
	private int releaseGhostSecondCounter;
	private int ghostScaredSecondCounter;
	private int ghostScatterChaseSecondCounter;
	private int ghostsReleased;
}
